########################################################################
#  Searx-qt - Lightweight desktop application for SearX.
#  Copyright (C) 2020  CYBERDEViL
#
#  This file is part of Searx-qt.
#
#  Searx-qt is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Searx-qt is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
########################################################################

from urllib.parse import urlparse
import random
import time
from operator import itemgetter

from PyQt5.QtCore import (
    QObject,
    pyqtSignal,
    QAbstractTableModel,
    QVariant,
    Qt
)

from searxqt.core.instances import Instances


class InstancesModel(QObject, Instances):
    changed = pyqtSignal()

    def __init__(self, requestSettings={}, parent=None):
        QObject.__init__(self, parent=parent)
        Instances.__init__(self)

        self._requestSettings = requestSettings
        self._lastUpdated = 0

    @property
    def requestSettings(self): return self._requestSettings

    def lastUpdated(self): return self._lastUpdated

    def data(self):
        data = {
            'instances': Instances.data(self),
            'lastUpdate': self._lastUpdated
        }
        return data

    def loadSettings(self, data):
        Instances.setData(self, data.get('instances', {}))
        self._lastUpdated = data.get('lastUpdate', 0)
        self.changed.emit()

    def setData(self, data):
        Instances.setData(self, data)
        self.changed.emit()

    def update(self):
        status, msg = Instances.update(
                self,
                requestKwargs=self.requestSettings.data
        )

        if status:
            self._lastUpdated = time.time()
            self.changed.emit()
        else:
            print("Update instances failed: `{0}`".format(msg))


class InstanceModelFilter(QObject):
    changed = pyqtSignal()

    def __init__(self, model, parent=None):
        QObject.__init__(self, parent=parent)
        """
        @type model: Instances
        """
        self._model = model
        self._current = model.copy()

        self._filter = {
            'ext': [],
            'skipExt': [],
            'version': [],
            'skipVersion': [],
            'skipUrl': [],
            'asnPrivacy': True,
            'ipv6': False,
            'engines': []
        }

        self._model.changed.connect(self.apply)

    def loadSettings(self, data):
        self.updateKwargs(
            {
                'ext': data.get('ext', []),
                'skipExt': data.get('skipExt', []),
                'version': data.get('version', []),
                'skipVersion': data.get('skipVersion', []),
                'skipUrl': data.get('skipUrl', []),
                'asnPrivacy': data.get('asnPrivacy', True),
                'ipv6': data.get('ipv6', False),
                #'engines': data.get('engines', [])  # TODO
            }
        )

    def saveSettings(self):
        return self.filter()

    def filter(self): return self._filter

    def parentModel(self): return self._model

    def updateKwargs(self, kwargs):
        for key in kwargs:
            self._filter[key] = kwargs[key]
        self.apply()

    def update(self):
        self._model.update()

    def apply(self):
        self._current.clear()
        for url, instance in self._model.items():
            # Url blacklist
            if self._filter['skipUrl']:
                if instance.url in self._filter['skipUrl']:
                    continue

            # Network
            if self._filter['skipExt'] or self._filter['ext']:
                parsedUrl = urlparse(instance.url)
                extSplit = parsedUrl.hostname.split('.')
                ext = extSplit[(len(extSplit)-1)]

                if self._filter['skipExt']:
                    if (ext in self._filter['skipExt']):
                        continue

                if self._filter['ext']:
                    if (ext not in self._filter['ext']):
                        continue

            # Version
            if self._filter['skipVersion'] or self._filter['version']:
                if self._filter['version']:
                    if instance.version not in self._filter['version']:
                        continue

            # ASN privacy
            if self._filter['asnPrivacy']:
                if instance.network.asnPrivacy != 0:
                    continue

            # IPv6
            if self._filter['ipv6']:
                if not instance.network.ipv6:
                    continue

            # Engines
            if self._filter['engines']:
                found = False
                for engine in self._filter['engines']:
                    for e in instance.engines:
                        if e.enabled and e.name == engine:
                            found = True
                            break

                if not found:
                    continue

            self._current.update({url: instance})

        self.changed.emit()

    def __contains__(self, url): return bool(url in self._current)

    def __iter__(self): return iter(self._current)

    def __getitem__(self, url): return self._current[url]

    def __str__(self): return str([url for url in self])

    def __repr__(self): return str(self)

    def __len__(self): return len(self._current)

    def items(self): return self._current.items()

    def keys(self): return self._current.keys()

    def values(self): return self._current.values()

    def copy(self): return self._current.copy()


class InstanceSelecterModel(QObject):
    optionsChanged = pyqtSignal()
    instanceChanged = pyqtSignal(str)  # instance url

    def __init__(self, model, parent=None):
        QObject.__init__(self, parent=parent)
        """
        @type model: Instances
        """
        self._model = model

        self._currentInstanceUrl = ''

        self._model.changed.connect(self.__modelChanged)

    def __modelChanged(self):
        """ This can happen after example blacklisting all instances.
        """
        if self.currentUrl and self.currentUrl not in self._model:
            self.currentUrl = ""

    @property
    def currentUrl(self): return self._currentInstanceUrl

    @currentUrl.setter
    def currentUrl(self, url):
        self._currentInstanceUrl = url
        self.instanceChanged.emit(url)

    def loadSettings(self, data):
        self.currentUrl = data.get('currentInstance', '')
        self.instanceChanged.emit(self.currentUrl)

    def saveSettings(self):
        return {
            'currentInstance': self.currentUrl
        }

    def getRandomInstances(self, amount=10):
        """ Returns a list of random instance urls.
        """
        return random.sample(list(self._model.keys()),
                             min(amount, len(self._model.keys())))

    def randomInstance(self):
        if self._model.keys():
            self.currentUrl = random.choice(list(self._model.keys()))
        return self.currentUrl


class InstanceTableModel(QAbstractTableModel):
    """ `InstancesModel` -> `QAbstractTableModel` adapter model
    """
    class Column:
        def __init__(self, name, route, type_):
            self._name = name
            self._route = route
            self._type = type_

        @property
        def type(self): return self._type

        @property
        def name(self): return self._name

        @property
        def route(self): return self._route

    def __init__(self, instancesModel, parent):
        """
        @param instancesModel: Resource model
        @type instancesModel: InstancesModel
        """
        QAbstractTableModel.__init__(self, parent)
        self._model = instancesModel

        self._keyIndex = []  # [key, key, ..]

        self._columns = [
            InstanceTableModel.Column('url', 'url', str),
            InstanceTableModel.Column('version', 'version', str),
            InstanceTableModel.Column('engines', 'engines', list),
            InstanceTableModel.Column('tls.version', 'tls.version', str),
            InstanceTableModel.Column(
                    'tls.cert.version',
                    'tls.certificate.version',
                    int),
            InstanceTableModel.Column(
                    'tls.countryName',
                    'tls.certificate.issuer.countryName',
                    str),
            InstanceTableModel.Column(
                    'tls.commonName',
                    'tls.certificate.issuer.commonName',
                    str),
            InstanceTableModel.Column(
                    'tls.organizationName',
                    'tls.certificate.issuer.organizationName',
                    str),
            InstanceTableModel.Column(
                    'network.asnPrivacy',
                    'network.asnPrivacy',
                    str),
            InstanceTableModel.Column(
                    'network.ipv6',
                    'network.ipv6',
                    bool),
            InstanceTableModel.Column('network.ips', 'network.ips', dict)
        ]

        instancesModel.changed.connect(self.__resourceModelChanged)

    def __genKeyIndexes(self):
        self._keyIndex.clear()
        for key in self._model:
            self._keyIndex.append(key)

    def __resourceModelChanged(self):
        self.layoutAboutToBeChanged.emit()
        self.__genKeyIndexes()
        # TODO re-select instanceSelecter currentUrl if still exists.
        self.layoutChanged.emit()

    def getColumns(self): return self._columns

    def getByIndex(self, index):
        """ Returns a Instance it's URL by index.

        @param index: Index of the instance it's url you like to get.
        @type index: int

        @return: Instance url
        @rtype: str
        """
        return self._keyIndex[index]

    def getByUrl(self, url):
        """ Returns a Instancs it's current index by url

        @param url: Url of the instance you want to get the current
                    index of.
        @type url: str

        @returns: Instance index.
        @rtype: int
        """
        return self._keyIndex.index(url)

    def getPropertyValueByIndex(self, index, route):
        obj = self._model[self.getByIndex(index)]
        return self.getPropertyValue(obj, route)

    def getPropertyValue(self, obj, route):
        """ Returns the `Instance` it's desired property.

        @param obj: instance object
        @type obj: Instance

        @param route: traversel path to value through properties.
        @type route: str
        """
        routes = route.split('.')
        propValue = None
        for propName in routes:
            propValue = getattr(obj, propName)
            obj = propValue

        return propValue

    """ QAbstractTableModel reimplementations below
    """
    def rowCount(self, parent): return len(self._model)

    def columnCount(self, parent): return len(self._columns)

    def sort(self, col, order=Qt.AscendingOrder):
        self.layoutAboutToBeChanged.emit()

        route = self._columns[col].route

        unsortedList = []
        for url, instance in self._model.items():
            value = str(
                        self.getPropertyValue(
                                instance,
                                route
                        )
                    )
            unsortedList.append([url, value])

        reverse = False if order == Qt.AscendingOrder else True

        sortedList = sorted(
                unsortedList,
                key=itemgetter(1),
                reverse=reverse
        )

        self._keyIndex.clear()
        for url, value in sortedList:
            self._keyIndex.append(url)

        self.layoutChanged.emit()

    def headerData(self, col, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return QVariant(self._columns[col].name)
        return QVariant()

    def data(self, index, role):
        if not index.isValid():
            return QVariant()

        if role == Qt.DisplayRole:
            value = self.getPropertyValueByIndex(
                        index.row(),
                        self._columns[index.column()].route)

            if index.column() == 2:  # engines
                newStr = ''
                for engine in value:
                    if engine.enabled:
                        if newStr:
                            newStr += ', {0}'.format(engine.name)
                        else:
                            newStr = engine.name
                return newStr

            elif index.column() == 10:  # ips
                return str(value)

            return value

        return QVariant()
